// 参考：https://blog.csdn.net/m0_38134431/article/details/118216181
const { Client } = require('ssh2');
const config = {
  host: '124.222.54.192',
  port: 22,
  username: 'administrator',
  password: 'chenliang970829@'
}


// 执行ssh连接
function doConnect(server, then) {
  const conn = new Client()
  conn.on('ready', function () {
    then && then(conn)
  }).on('error', function (err) {
    // console.error('connect error!', err)
  }).on('close', function () {
    conn.end()
  }).connect(server)
}


function Shell(server, cmd, then) {
  doConnect(server, (conn) => {
    conn.shell((err, stream) => {
      if (err) {
        then(err);
      } else { // end of if
        let buf = '';
        stream.on('close', () => {
          conn.end();
          then(err, buf);
        }).on('data', (data) => {
          buf += data;
        }).stderr.on('data', (data) => {
          console.log(`stderr: ${data}`);
        });
        stream.end(cmd);
      }
    });
  });
}

/**
 * 上传文件
 * @param {*} server 远程电脑凭证
 * @param {*} localPath 本地文件路径
 * @param {*} remotePath 远程需要上传的文件路径(包含文件名称)
 * @param {*} then 回调：then(err, result)
 */
function UploadFile(server, localPath, remotePath, then) {
  doConnect(server, (conn) => {
    conn.sftp((err, sftp) => {
      if (err) {
        then(err);
      } else {
        sftp.fastPut(localPath, remotePath, (err, result) => {
          conn.end();
          then(err, result);
        });
      }
    });
  });
}

/**
 * 描述：控制上传或者下载一个一个的执行
 */
const util = require('util')
const fs = require('fs')
const path = require('path')
const events = require('events')
function Control() {
  events.EventEmitter.call(this);
}
util.inherits(Control, events.EventEmitter); // 使这个类继承EventEmitter
const control = new Control();
control.on('donext', (todos, then) => {
  if (todos.length > 0) {
    const func = todos.shift();
    func((err, result) => {
      if (err) {
        throw err;
        then(err);
      } else {
        control.emit('donext', todos, then);
      }
    });
  } else {
    then(null);
  }
});



/**
 * 描述：获取windows上的文件目录以及文件列表信息
 * 参数：destDir 本地路径，
 *       dirs 目录列表
 *       files 文件列表
 */
function GetFileAndDirList(localDir, dirs, files) {
  const dir = fs.readdirSync(localDir);
  for (let i = 0; i < dir.length; i++) {
    const p = path.join(localDir, dir[i]);
    const stat = fs.statSync(p);
    if (stat.isDirectory()) {
      dirs.push(p);
      GetFileAndDirList(p, dirs, files);
    } else {
      files.push(p);
    }
  }
}


/**
 * 上传文件夹到远程目录
 * @param {*} server 远程电脑凭证
 * @param {*} localDir 本地路径
 * @param {*} remoteDir 远程路径: 一定要存在，否则会报 No such fire or directory
 * @param {*} then then(err)
 * 缺点：需要先删除目录，然后创建目录，最后才上传文件，如果目录有30000个，那就需要等30000走完了才走下一步
 * 优化：可以考虑上传完一个文件夹后立马上传文件，同时将dirs优化成嵌套层次的数据，然后直接将最底层的子集目录创建即可，因为使用 mkdir -p C:/demo/test/my-test 时，如果 demo、test、my-test都不存在，该命令都会创建同时会按照我们给定的层次创建.
 * 同时应该还需要考虑备份问题，根据某一次部署节点来备份数据(类似于云效平台)
 */
function UploadDir(server, localDir, remoteDir, then) {
  const dirs = [];
  const files = [];
  GetFileAndDirList(localDir, dirs, files);

  // 删除远程指定目录下的所有文件
  const deleteDir = [(done) => {
    const cmd = `rm -rf ${remoteDir}* \r\nexit\r\n`;
    console.log('clear files: ', remoteDir)
    Shell(server, cmd, done);
  }];

  // 创建远程目录
  const todoDir = [];
  dirs.forEach((dir) => {
    todoDir.push((done) => {
      const to = path.join(remoteDir, dir.slice(localDir.length - 1));
      const cmd = `mkdir -p ${to}\r\nexit\r\n`;
      console.log('mkdir: ', to)
      Shell(server, cmd, done);
    });
  });

  // 上传文件
  const todoFile = [];
  files.forEach((file) => {
    todoFile.push((done) => {
      const to = path.join(remoteDir, file.slice(localDir.length - 1)).replace(/[\\]/g, '/');
      console.log('upload file: ', to)
      UploadFile(server, file, to, done);
    });
  });

  // 递归执行：清空文件 ->  创建目录 -> 上传文件
  control.emit('donext', deleteDir, (err) => {
    if (err) throw err
    control.emit('donext', todoDir, (err) => {
      if (err) throw err
      control.emit('donext', todoFile, then);
    });
  });
}
UploadDir(config, './dist', 'C:\\test\\my-test', (err, res) => {
  if (err) {
    console.log('error: ', err)
  }
})